
var CTM = CTM || {};

CTM.CompressionMethod =
{
	RAW: 0x00574152,
	MG1: 0x0031474d,
	MG2: 0x0032474d
};

CTM.Flags =
{
	NORMALS: 0x00000001
};

CTM.File = function(stream)
{
	this.load(stream);
};

CTM.File.prototype.load = function(stream)
{
	this.header = new CTM.FileHeader(stream);

	this.body = new CTM.FileBody(this.header);

	this.getReader().read(stream, this.body);
};

CTM.File.prototype.getReader = function()
{
	var reader;

	switch(this.header.compressionMethod)
	{
	case CTM.CompressionMethod.RAW:
		reader = new CTM.ReaderRAW();
		break;

	case CTM.CompressionMethod.MG1:
		reader = new CTM.ReaderMG1();
		break;

	case CTM.CompressionMethod.MG2:
		reader = new CTM.ReaderMG2();
		break;
	}

	return reader;
};

CTM.FileHeader = function(stream)
{
	stream.readInt32(); //magic "OCTM"
	this.fileFormat = stream.readInt32();
	this.compressionMethod = stream.readInt32();
	this.vertexCount = stream.readInt32();
	this.triangleCount = stream.readInt32();
	this.uvMapCount = stream.readInt32();
	this.attrMapCount = stream.readInt32();
	this.flags = stream.readInt32();
	this.comment = stream.readString();
};

CTM.FileHeader.prototype.hasNormals = function()
{
	return this.flags & CTM.Flags.NORMALS;
};

CTM.FileBody = function(header)
{
	var i = header.triangleCount * 3,
	    v = header.vertexCount * 3,
	    n = header.hasNormals()? header.vertexCount * 3: 0,
	    u = header.vertexCount * 2,
	    a = header.vertexCount * 4,
	    j = 0;

	var data = new ArrayBuffer(
	    (i + v + n + (u * header.uvMapCount) + (a * header.attrMapCount) ) * 4);

	this.indices = new Uint32Array(data, 0, i);

	this.vertices = new Float32Array(data, i * 4, v);

	if ( header.hasNormals() )
	{
		this.normals = new Float32Array(data, (i + v) * 4, n);
	}

	if (header.uvMapCount)
	{
		this.uvMaps = [];

		for (j = 0; j < header.uvMapCount; ++ j)
		{
			this.uvMaps[j] = {uv: new Float32Array(data, (i + v + n + (j * u) ) * 4, u)};
		}
	}

	if (header.attrMapCount)
	{
		this.attrMaps = [];

		for (j = 0; j < header.attrMapCount; ++ j)
		{
			this.attrMaps[j] = {attr: new Float32Array(data, (i + v + n + (u * header.uvMapCount) + (j * a) ) * 4, a) };
		}
	}
};

CTM.FileMG2Header = function(stream)
{
	stream.readInt32(); //magic "MG2H"
	this.vertexPrecision = stream.readFloat32();
	this.normalPrecision = stream.readFloat32();
	this.lowerBoundx = stream.readFloat32();
	this.lowerBoundy = stream.readFloat32();
	this.lowerBoundz = stream.readFloat32();
	this.higherBoundx = stream.readFloat32();
	this.higherBoundy = stream.readFloat32();
	this.higherBoundz = stream.readFloat32();
	this.divx = stream.readInt32();
	this.divy = stream.readInt32();
	this.divz = stream.readInt32();

	this.sizex = (this.higherBoundx - this.lowerBoundx) / this.divx;
	this.sizey = (this.higherBoundy - this.lowerBoundy) / this.divy;
	this.sizez = (this.higherBoundz - this.lowerBoundz) / this.divz;
};

CTM.ReaderRAW = function()
{
};

CTM.ReaderRAW.prototype.read = function(stream, body)
{
	this.readIndices(stream, body.indices);
	this.readVertices(stream, body.vertices);

	if (body.normals)
	{
		this.readNormals(stream, body.normals);
	}

	if (body.uvMaps)
	{
		this.readUVMaps(stream, body.uvMaps);
	}

	if (body.attrMaps)
	{
		this.readAttrMaps(stream, body.attrMaps);
	}
};

CTM.ReaderRAW.prototype.readIndices = function(stream, indices)
{
	stream.readInt32(); //magic "INDX"
	stream.readArrayInt32(indices);
};

CTM.ReaderRAW.prototype.readVertices = function(stream, vertices)
{
	stream.readInt32(); //magic "VERT"
	stream.readArrayFloat32(vertices);
};

CTM.ReaderRAW.prototype.readNormals = function(stream, normals)
{
	stream.readInt32(); //magic "NORM"
	stream.readArrayFloat32(normals);
};

CTM.ReaderRAW.prototype.readUVMaps = function(stream, uvMaps)
{
	var i = 0;

	for (; i < uvMaps.length; ++ i)
	{
		stream.readInt32(); //magic "TEXC"

		uvMaps[i].name = stream.readString();
		uvMaps[i].filename = stream.readString();
		stream.readArrayFloat32(uvMaps[i].uv);
	}
};

CTM.ReaderRAW.prototype.readAttrMaps = function(stream, attrMaps)
{
	var i = 0;

	for (; i < attrMaps.length; ++ i)
	{
		stream.readInt32(); //magic "ATTR"

		attrMaps[i].name = stream.readString();
		stream.readArrayFloat32(attrMaps[i].attr);
	}
};

CTM.ReaderMG1 = function()
{
};

CTM.ReaderMG1.prototype.read = function(stream, body)
{
	this.readIndices(stream, body.indices);
	this.readVertices(stream, body.vertices);

	if (body.normals)
	{
		this.readNormals(stream, body.normals);
	}

	if (body.uvMaps)
	{
		this.readUVMaps(stream, body.uvMaps);
	}

	if (body.attrMaps)
	{
		this.readAttrMaps(stream, body.attrMaps);
	}
};

CTM.ReaderMG1.prototype.readIndices = function(stream, indices)
{
	stream.readInt32(); //magic "INDX"
	stream.readInt32(); //packed size

	var interleaved = new CTM.InterleavedStream(indices, 3);
	LZMA.decompress(stream, stream, interleaved, interleaved.data.length);

	CTM.restoreIndices(indices, indices.length);
};

CTM.ReaderMG1.prototype.readVertices = function(stream, vertices)
{
	stream.readInt32(); //magic "VERT"
	stream.readInt32(); //packed size

	var interleaved = new CTM.InterleavedStream(vertices, 1);
	LZMA.decompress(stream, stream, interleaved, interleaved.data.length);
};

CTM.ReaderMG1.prototype.readNormals = function(stream, normals)
{
	stream.readInt32(); //magic "NORM"
	stream.readInt32(); //packed size

	var interleaved = new CTM.InterleavedStream(normals, 3);
	LZMA.decompress(stream, stream, interleaved, interleaved.data.length);
};

CTM.ReaderMG1.prototype.readUVMaps = function(stream, uvMaps)
{
	var i = 0;

	for (; i < uvMaps.length; ++ i)
	{
		stream.readInt32(); //magic "TEXC"

		uvMaps[i].name = stream.readString();
		uvMaps[i].filename = stream.readString();

		stream.readInt32(); //packed size

		var interleaved = new CTM.InterleavedStream(uvMaps[i].uv, 2);
		LZMA.decompress(stream, stream, interleaved, interleaved.data.length);
	}
};

CTM.ReaderMG1.prototype.readAttrMaps = function(stream, attrMaps)
{
	var i = 0;

	for (; i < attrMaps.length; ++ i)
	{
		stream.readInt32(); //magic "ATTR"

		attrMaps[i].name = stream.readString();

		stream.readInt32(); //packed size

		var interleaved = new CTM.InterleavedStream(attrMaps[i].attr, 4);
		LZMA.decompress(stream, stream, interleaved, interleaved.data.length);
	}
};

CTM.ReaderMG2 = function()
{
};

CTM.ReaderMG2.prototype.read = function(stream, body)
{
	this.MG2Header = new CTM.FileMG2Header(stream);

	this.readVertices(stream, body.vertices);
	this.readIndices(stream, body.indices);

	if (body.normals)
	{
		this.readNormals(stream, body);
	}

	if (body.uvMaps)
	{
		this.readUVMaps(stream, body.uvMaps);
	}

	if (body.attrMaps)
	{
		this.readAttrMaps(stream, body.attrMaps);
	}
};

CTM.ReaderMG2.prototype.readVertices = function(stream, vertices)
{
	stream.readInt32(); //magic "VERT"
	stream.readInt32(); //packed size

	var interleaved = new CTM.InterleavedStream(vertices, 3);
	LZMA.decompress(stream, stream, interleaved, interleaved.data.length);

	var gridIndices = this.readGridIndices(stream, vertices);

	CTM.restoreVertices(vertices, this.MG2Header, gridIndices, this.MG2Header.vertexPrecision);
};

CTM.ReaderMG2.prototype.readGridIndices = function(stream, vertices)
{
	stream.readInt32(); //magic "GIDX"
	stream.readInt32(); //packed size

	var gridIndices = new Uint32Array(vertices.length / 3);

	var interleaved = new CTM.InterleavedStream(gridIndices, 1);
	LZMA.decompress(stream, stream, interleaved, interleaved.data.length);

	CTM.restoreGridIndices(gridIndices, gridIndices.length);

	return gridIndices;
};

CTM.ReaderMG2.prototype.readIndices = function(stream, indices)
{
	stream.readInt32(); //magic "INDX"
	stream.readInt32(); //packed size

	var interleaved = new CTM.InterleavedStream(indices, 3);
	LZMA.decompress(stream, stream, interleaved, interleaved.data.length);

	CTM.restoreIndices(indices, indices.length);
};

CTM.ReaderMG2.prototype.readNormals = function(stream, body)
{
	stream.readInt32(); //magic "NORM"
	stream.readInt32(); //packed size

	var interleaved = new CTM.InterleavedStream(body.normals, 3);
	LZMA.decompress(stream, stream, interleaved, interleaved.data.length);

	var smooth = CTM.calcSmoothNormals(body.indices, body.vertices);

	CTM.restoreNormals(body.normals, smooth, this.MG2Header.normalPrecision);
};

CTM.ReaderMG2.prototype.readUVMaps = function(stream, uvMaps)
{
	var i = 0;

	for (; i < uvMaps.length; ++ i)
	{
		stream.readInt32(); //magic "TEXC"

		uvMaps[i].name = stream.readString();
		uvMaps[i].filename = stream.readString();

		var precision = stream.readFloat32();

		stream.readInt32(); //packed size

		var interleaved = new CTM.InterleavedStream(uvMaps[i].uv, 2);
		LZMA.decompress(stream, stream, interleaved, interleaved.data.length);

		CTM.restoreMap(uvMaps[i].uv, 2, precision);
	}
};

CTM.ReaderMG2.prototype.readAttrMaps = function(stream, attrMaps)
{
	var i = 0;

	for (; i < attrMaps.length; ++ i)
	{
		stream.readInt32(); //magic "ATTR"

		attrMaps[i].name = stream.readString();

		var precision = stream.readFloat32();

		stream.readInt32(); //packed size

		var interleaved = new CTM.InterleavedStream(attrMaps[i].attr, 4);
		LZMA.decompress(stream, stream, interleaved, interleaved.data.length);

		CTM.restoreMap(attrMaps[i].attr, 4, precision);
	}
};

CTM.restoreIndices = function(indices, len)
{
	var i = 3;

	if (len > 0)
	{
		indices[2] += indices[0];
	}

	for (; i < len; i += 3)
	{
		indices[i] += indices[i - 3];

		if (indices[i] === indices[i - 3])
		{
			indices[i + 1] += indices[i - 2];
		}
		else
		{
			indices[i + 1] += indices[i];
		}

		indices[i + 2] += indices[i];
	}
};

CTM.restoreGridIndices = function(gridIndices, len)
{
	var i = 1;

	for (; i < len; ++ i)
	{
		gridIndices[i] += gridIndices[i - 1];
	}
};

CTM.restoreVertices = function(vertices, grid, gridIndices, precision)
{
	var gridIdx, delta, x, y, z,
	    intVertices = new Uint32Array(vertices.buffer, vertices.byteOffset, vertices.length),
	ydiv = grid.divx, zdiv = ydiv * grid.divy,
	prevGridIdx = 0x7fffffff, prevDelta = 0,
	i = 0, j = 0, len = gridIndices.length;

	for (; i < len; j += 3)
	{
		x = gridIdx = gridIndices[i ++];

		z = ~~(x / zdiv);
		x -= ~~(z * zdiv);
		y = ~~(x / ydiv);
		x -= ~~(y * ydiv);

		delta = intVertices[j];

		if (gridIdx === prevGridIdx)
		{
			delta += prevDelta;
		}

		vertices[j]     = grid.lowerBoundx +
		                  x * grid.sizex + precision * delta;
		vertices[j + 1] = grid.lowerBoundy +
		                  y * grid.sizey + precision * intVertices[j + 1];
		vertices[j + 2] = grid.lowerBoundz +
		                  z * grid.sizez + precision * intVertices[j + 2];

		prevGridIdx = gridIdx;
		prevDelta = delta;
	}
};

CTM.restoreNormals = function(normals, smooth, precision)
{
	var ro, phi, theta, sinPhi,
	    nx, ny, nz, by, bz, len,
	    intNormals = new Uint32Array(normals.buffer, normals.byteOffset, normals.length),
	i = 0, k = normals.length,
	PI_DIV_2 = 3.141592653589793238462643 * 0.5;

	for (; i < k; i += 3)
	{
		ro = intNormals[i] * precision;
		phi = intNormals[i + 1];

		if (phi === 0)
		{
			normals[i]     = smooth[i]     * ro;
			normals[i + 1] = smooth[i + 1] * ro;
			normals[i + 2] = smooth[i + 2] * ro;
		}
		else
		{

			if (phi <= 4)
			{
				theta = (intNormals[i + 2] - 2) * PI_DIV_2;
			}
			else
			{
				theta = ( (intNormals[i + 2] * 4 / phi) - 2) * PI_DIV_2;
			}

			phi *= precision * PI_DIV_2;
			sinPhi = ro * Math.sin(phi);

			nx = sinPhi * Math.cos(theta);
			ny = sinPhi * Math.sin(theta);
			nz = ro * Math.cos(phi);

			bz = smooth[i + 1];
			by = smooth[i] - smooth[i + 2];

			len = Math.sqrt(2 * bz * bz + by * by);

			if (len > 1e-20)
			{
				by /= len;
				bz /= len;
			}

			normals[i]     = smooth[i]     * nz +
			                 (smooth[i + 1] * bz - smooth[i + 2] * by) * ny - bz * nx;
			normals[i + 1] = smooth[i + 1] * nz -
			                 (smooth[i + 2]      + smooth[i]   ) * bz  * ny + by * nx;
			normals[i + 2] = smooth[i + 2] * nz +
			                 (smooth[i]     * by + smooth[i + 1] * bz) * ny + bz * nx;
		}
	}
};

CTM.restoreMap = function(map, count, precision)
{
	var delta, value,
	    intMap = new Uint32Array(map.buffer, map.byteOffset, map.length),
	i = 0, j, len = map.length;

	for (; i < count; ++ i)
	{
		delta = 0;

		for (j = i; j < len; j += count)
		{
			value = intMap[j];

			delta += value & 1? -( (value + 1) >> 1): value >> 1;

			map[j] = delta * precision;
		}
	}
};

CTM.calcSmoothNormals = function(indices, vertices)
{
	var smooth = new Float32Array(vertices.length),
	indx, indy, indz, nx, ny, nz,
	v1x, v1y, v1z, v2x, v2y, v2z, len,
	i, k;

	for (i = 0, k = indices.length; i < k;)
	{
		indx = indices[i ++] * 3;
		indy = indices[i ++] * 3;
		indz = indices[i ++] * 3;

		v1x = vertices[indy]     - vertices[indx];
		v2x = vertices[indz]     - vertices[indx];
		v1y = vertices[indy + 1] - vertices[indx + 1];
		v2y = vertices[indz + 1] - vertices[indx + 1];
		v1z = vertices[indy + 2] - vertices[indx + 2];
		v2z = vertices[indz + 2] - vertices[indx + 2];

		nx = v1y * v2z - v1z * v2y;
		ny = v1z * v2x - v1x * v2z;
		nz = v1x * v2y - v1y * v2x;

		len = Math.sqrt(nx * nx + ny * ny + nz * nz);

		if (len > 1e-10)
		{
			nx /= len;
			ny /= len;
			nz /= len;
		}

		smooth[indx]     += nx;
		smooth[indx + 1] += ny;
		smooth[indx + 2] += nz;
		smooth[indy]     += nx;
		smooth[indy + 1] += ny;
		smooth[indy + 2] += nz;
		smooth[indz]     += nx;
		smooth[indz + 1] += ny;
		smooth[indz + 2] += nz;
	}

	for (i = 0, k = smooth.length; i < k; i += 3)
	{
		len = Math.sqrt(smooth[i] * smooth[i] +
		                smooth[i + 1] * smooth[i + 1] +
		                smooth[i + 2] * smooth[i + 2]);

		if(len > 1e-10)
		{
			smooth[i]     /= len;
			smooth[i + 1] /= len;
			smooth[i + 2] /= len;
		}
	}

	return smooth;
};

CTM.isLittleEndian = (function()
{
	var buffer = new ArrayBuffer(2),
	bytes = new Uint8Array(buffer),
	ints = new Uint16Array(buffer);

	bytes[0] = 1;

	return ints[0] === 1;
}());

CTM.InterleavedStream = function(data, count)
{
	this.data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
	this.offset = CTM.isLittleEndian? 3: 0;
	this.count = count * 4;
	this.len = this.data.length;
};

CTM.InterleavedStream.prototype.writeByte = function(value)
{
	this.data[this.offset] = value;

	this.offset += this.count;

	if (this.offset >= this.len)
	{

		this.offset -= this.len - 4;

		if (this.offset >= this.count)
		{

			this.offset -= this.count + (CTM.isLittleEndian? 1: -1);
		}
	}
};

CTM.Stream = function(data)
{
	this.data = data;
	this.offset = 0;
};

CTM.Stream.prototype.TWO_POW_MINUS23 = Math.pow(2, -23);

CTM.Stream.prototype.TWO_POW_MINUS126 = Math.pow(2, -126);

CTM.Stream.prototype.readByte = function()
{
	return this.data.charCodeAt(this.offset ++) & 0xff;
};

CTM.Stream.prototype.readInt32 = function()
{
	var i = this.readByte();
	i |= this.readByte() << 8;
	i |= this.readByte() << 16;
	return i | (this.readByte() << 24);
};

CTM.Stream.prototype.readFloat32 = function()
{
	var m = this.readByte();
	m += this.readByte() << 8;

	var b1 = this.readByte();
	var b2 = this.readByte();

	m += (b1 & 0x7f) << 16;
	var e = ( (b2 & 0x7f) << 1) | ( (b1 & 0x80) >>> 7);
	var s = b2 & 0x80? -1: 1;

	if (e === 255)
	{
		return m !== 0? NaN: s * Infinity;
	}

	if (e > 0)
	{
		return s * (1 + (m * this.TWO_POW_MINUS23) ) * Math.pow(2, e - 127);
	}

	if (m !== 0)
	{
		return s * m * this.TWO_POW_MINUS126;
	}

	return s * 0;
};

CTM.Stream.prototype.readString = function()
{
	var len = this.readInt32();

	this.offset += len;

	return this.data.substr(this.offset - len, len);
};

CTM.Stream.prototype.readArrayInt32 = function(array)
{
	var i = 0, len = array.length;

	while(i < len)
	{
		array[i ++] = this.readInt32();
	}

	return array;
};

CTM.Stream.prototype.readArrayFloat32 = function(array)
{
	var i = 0, len = array.length;

	while(i < len)
	{
		array[i ++] = this.readFloat32();
	}

	return array;
};
